#include "nethelp.h"
#include <memory>

#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")

template<typename enum_proc>
bool enum_adapters(enum_proc proc)
{
    ULONG sizeRequired = 0;
    ULONG retCode = 0;
    ULONG gaaFlags = GAA_FLAG_SKIP_MULTICAST |
        GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_INCLUDE_GATEWAYS;

    if( ERROR_BUFFER_OVERFLOW !=
        GetAdaptersAddresses(AF_INET, gaaFlags, 0, 0, &sizeRequired) )
    {
        return false;
    }

    std::vector<char> addrBuf;
    addrBuf.assign(sizeRequired+2, 0);

    PIP_ADAPTER_ADDRESSES pAddresses = 0;
    pAddresses = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(&(addrBuf[0]));

    if( NO_ERROR !=
        GetAdaptersAddresses(AF_INET, gaaFlags, 0, pAddresses, &sizeRequired) )
    {
        return false;
    }

    for(PIP_ADAPTER_ADDRESSES it = pAddresses; it != 0; it = it->Next)
    {
        if( !proc(it) )
        {
            return true;
        }
    }

    return true;
}

class addr_enum_proc
{
    std::vector<adapter_inf>* adl;

public:
    addr_enum_proc(std::vector<adapter_inf>* p): adl(p){}
    addr_enum_proc(const addr_enum_proc& r): adl(r.adl){}

    bool operator ()(PIP_ADAPTER_ADDRESSES it)
    {
        BOOL useDefaultChar = FALSE;
        char nameBuf[2048];
        char addrString[32];
        DWORD addrStringLength;

        adapter_inf inf;
        memset(nameBuf, 0, sizeof(nameBuf));
        if( it->FriendlyName )
        {
            WideCharToMultiByte(
                CP_UTF8, 0,
                it->FriendlyName, wcslen(it->FriendlyName),
                nameBuf, 2044,
                0, 0
                );
        }
        inf.name = nameBuf;

        memset(addrString, 0, sizeof(addrString));
        if( it->FirstUnicastAddress )
        {
            addrStringLength = sizeof(addrString) - 2;
            WSAAddressToStringA(
                it->FirstUnicastAddress->Address.lpSockaddr,
                it->FirstUnicastAddress->Address.iSockaddrLength,
                0, addrString, &addrStringLength);
        }
        inf.unicast = addrString;

        memset(addrString, 0, sizeof(addrString));
        if( it->FirstGatewayAddress )
        {
            addrStringLength = sizeof(addrString) - 2;
            WSAAddressToStringA(
                it->FirstGatewayAddress->Address.lpSockaddr,
                it->FirstGatewayAddress->Address.iSockaddrLength,
                0, addrString, &addrStringLength);
        }
        inf.gateway = addrString;

        if( it->FirstDnsServerAddress )
        {
            addrStringLength = sizeof(addrString) - 2;
            WSAAddressToStringA(
                it->FirstDnsServerAddress->Address.lpSockaddr,
                it->FirstDnsServerAddress->Address.iSockaddrLength,
                0, addrString, &addrStringLength);
        }
        inf.dns = addrString;

        inf.luid = it->Luid;

        adl->push_back(inf);
        return true;
    }

};

bool get_adapter_list(std::vector<adapter_inf>& adl)
{
    return enum_adapters(addr_enum_proc(&adl));
}

bool is_ipv4_address( const std::string& s )
{
    if( s.length() < 7 || s.length() > 15 )
    {
        return false;
    }

    SOCKADDR sa;
    int sa_len = sizeof(sa);
    return !WSAStringToAddressA((LPSTR)s.c_str(), AF_INET, NULL, &sa, &sa_len);
}

void sock_startup()
{
    WSADATA wd = {0};
    ::WSAStartup(MAKEWORD(2,2), &wd);
}

void sock_cleanup()
{
    ::WSACleanup();
}

bool make_route(bool enable, char* dst, int masks, const adapter_inf& inf, size_t metric)
{
    MIB_IPFORWARD_ROW2 forward_row;
    InitializeIpForwardEntry(&forward_row);
    
    // Interface
    // NOTE that inf.luid may be null(zero) here
    forward_row.InterfaceLuid = inf.luid;

    int addrlen;
    
    // Destination
    addrlen = sizeof(SOCKADDR_IN);
    forward_row.DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET;
    forward_row.DestinationPrefix.PrefixLength = masks;
    if( WSAStringToAddressA(dst, AF_INET, NULL,
        (LPSOCKADDR)&(forward_row.DestinationPrefix.Prefix.Ipv4), &addrlen) )
    {
        return false;
    }

    // Gateway
    addrlen = sizeof(SOCKADDR_IN);
    if( WSAStringToAddressA((LPSTR)(inf.gateway.c_str()), AF_INET, NULL,
        (LPSOCKADDR)&(forward_row.NextHop.Ipv4), &addrlen) )
    {
        // It's OK to delete routes without gateway specified
        if( enable ){ return false; }

        // Since gateway not specified, we need to find out it
        // Or call of DeleteIpForwardEntry2 function will fail
        GetIpForwardEntry2(&forward_row);
    }

    DWORD err;

    if( enable )
    {
        // Metric
        if( metric != size_t(-1) )
        {
            forward_row.Metric = metric;
        }

        // Static route
        forward_row.Protocol = MIB_IPPROTO_NT_STATIC;

        err = CreateIpForwardEntry2(&forward_row);
    }
    else
    {
        err = DeleteIpForwardEntry2(&forward_row);
    }

    return err == NO_ERROR;
}
